﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Threading;
using System.IO;
using MySql.Data.MySqlClient;
using System.Net.Sockets;
using CustomNetworking;

namespace Boggle
{
    /// <summary>
    /// Listens for HTTP requests on port 2500 and responds with
    /// information about Boggle games and players.
    /// </summary>
    public class WebServer
    {
        private TcpListener tcpListener;

        private readonly string connectionString =
            "server=atr.eng.utah.edu;database=ahbradfo;uid=ahbradfo;password=00616767";

        /// <summary>
        /// Creates a new WebServer listening on port 2500;
        /// </summary>
        public WebServer()
        {
            tcpListener = new TcpListener(IPAddress.Any, 2500);
        }

        /// <summary>
        /// Starts listening for incoming requests.
        /// </summary>
        public void Start()
        {
            tcpListener.Start();
            tcpListener.BeginAcceptSocket(ConnectionCallback, tcpListener);
        }

        /// <summary>
        /// Stops the underlying TcpListener.
        /// </summary>
        public void Stop() { tcpListener.Stop(); }

        /// <summary>
        /// Creates a new StringSocket when a new connection is made.
        /// </summary>
        public void ConnectionCallback(IAsyncResult result)
        {
            try
            {
                TcpListener listener = (TcpListener)result.AsyncState;

                //create get the socket and wrap it in a string socket
                Socket socket = listener.EndAcceptSocket(result);
                StringSocket client = new StringSocket(socket, new UTF8Encoding());

                // listen for messages from the client
                client.BeginReceive(ReceiveCallback, client);

                // start listening for new connection requests
                listener.BeginAcceptSocket(ConnectionCallback, tcpListener);
            }
            catch (Exception)
            {
                // in case something goes wonky
                Console.WriteLine("Connection Error");
            }
        }

        /// <summary>
        /// Receives the GET strings from the clients and sends them to be processed.
        /// </summary>
        public void ReceiveCallback(string s, Exception e, object payload)
        {
            if (s != null && s.Length > 0)
            {
                string request = s.Replace("\r", "");
                string[] tokens = request.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                ThreadPool.QueueUserWorkItem(x => ProcessRequest(tokens[1], (StringSocket)payload));
            }
        }

        /// <summary>
        /// Just a dummy method.
        /// </summary>
        public void SendCallback(Exception e, object p)
        {
            ManualResetEvent mre = (ManualResetEvent)p;
            mre.Set();
        }

        /// <summary>
        /// Sends the msg (could be HTML) to the client and closes the connection.
        /// </summary>
        private void SendPageAndClose(StringSocket client, string msg)
        {
            ManualResetEvent mre = new ManualResetEvent(false);

            // send the HTTP preamble
            client.BeginSend("HTTP/1.1 200 OK\r\n" +
                             "Connection: close\r\n" +
                             "Content-Type: text/html; charset=UTF-8\r\n" +
                             "\r\n", SendCallback, mre);

            // wait until the first message is sent
            mre.WaitOne(1000);
            mre.Reset();

            // send the message and close the socket
            client.BeginSend(msg, SendCallback, mre);
            mre.WaitOne(1000);

            client.CloseSocket(5);
        }

        /// <summary>
        /// Parses the URL and determines what type of request has been made.
        /// </summary>
        private void ProcessRequest(string request, StringSocket client)
        {
            lock (tcpListener)
            {
                try
                {
                    // restore the spaces in the string
                    request = request.Replace("%20", " ");

                    // tokenize the request
                    string[] tokens = request.Split(new char[] { '/', '?', '=' },
                        StringSplitOptions.RemoveEmptyEntries);

                    // determine how to handle the request
                    if (tokens.Length == 0)
                        SendPageAndClose(client, "<h2>Resource not found</h2>\r\n");
                    else if (tokens[0] == "players" && tokens.Length == 1)
                        ProcessPlayersRequest(client);
                    else if (tokens[0] == "games" && tokens.Length == 3)
                    {
                        ProcessGamesRequest(client, tokens[2]);
                    }
                    else if (tokens[0] == "game" && tokens.Length == 3)
                        ProcessGameRequest(client, tokens[2]);
                    else
                        SendPageAndClose(client, "<h2>Resource not found</h2>\r\n");
                }
                catch (Exception ex) { Console.WriteLine("Request error: " + ex); }
            }
        }

        /// <summary>
        /// Retrieves all of the players from the DB and returns an HTML page with the results.
        /// </summary>
        private void ProcessPlayersRequest(StringSocket client)
        {
            List<PlayerRecord> records = new List<PlayerRecord>();

            #region SQL stuff
            // Connect to the DB
            using (MySqlConnection connection = new MySqlConnection(connectionString))
            {
                try
                {
                    connection.Open();
                    MySqlCommand command = connection.CreateCommand();

                    command.CommandText = "SELECT * FROM Players;";
                    MySqlDataReader reader = command.ExecuteReader();

                    while (reader.Read())
                    {
                        int id = (int)reader[0];
                        string name = (string)reader[1];
                        int wins = (int)reader[2];
                        int losses = (int)reader[3];
                        int ties = (int)reader[4];

                        records.Add(new PlayerRecord(id, name, wins, losses, ties));
                    }
                    reader.Close();
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
                finally
                {
                    connection.Close();
                }
            }
            #endregion

            #region html generation
            List<string> tableHeaders = new List<string> { "Name", "Games Won", "Games Lost", "Games Tied" };

            StringBuilder sb = new StringBuilder();

            sb.Append("<html>\n<head>\n<title>BOGGLE!</title>\n</head>\n<body>\n");
            sb.Append("<h2>Boggle Players</h2>\n");

            // set up the table
            sb.Append("<table border=\"1\">\n<tr>\n");

            // set the headers
            foreach (string s in tableHeaders)
                sb.AppendFormat("<th width=\"100\">{0}</th>\n", s);

            sb.Append("</tr>\n");

            // create the rows
            #region
            foreach (PlayerRecord r in records)
            {
                sb.Append("<tr align=\"center\">\n");

                sb.AppendFormat("<td>{0}</td>\n", r.name);
                sb.AppendFormat("<td>{0}</td>\n", r.wins);
                sb.AppendFormat("<td>{0}</td>\n", r.losses);
                sb.AppendFormat("<td>{0}</td>\n", r.ties);

                sb.Append("</tr>");
            }
            #endregion

            sb.Append("</table>\n");
            sb.Append("</body>\n");
            sb.Append("</html>");

            SendPageAndClose(client, sb.ToString() + "\r\n");

            #endregion            
        }

        /// <summary>
        /// Retrieves the statistics for a particular game and returns an HTML summary.
        /// </summary>
        private void ProcessGameRequest(StringSocket client, string gameNum)
        {
            int gameID = 0;

            if (!Int32.TryParse(gameNum, out gameID))
            {
                SendPageAndClose(client, "<h2>Resource not found</h2>\r\n");
                return;
            }

            GameRecord record = new GameRecord();
            List<string> p1Legal = new List<string>();
            List<string> p1Illegal = new List<string>();
            List<string> p2Legal = new List<string>();
            List<string> p2Illegal = new List<string>();
            List<string> common = new List<string>();

            #region SQL stuff
            // Connect to the DB
            using (MySqlConnection connection = new MySqlConnection(connectionString))
            {
                try
                {
                    connection.Open();
                    MySqlCommand command = connection.CreateCommand();

                    command.CommandText =
                        "SELECT g.Date, p1.Name, p2.Name, g.Player1Score, g.Player2Score, g.GameTime, g.Board " +
                        "FROM Games g " +
                        "JOIN Players p1 " +
                        "ON g.ID = @id AND p1.ID = g.Player1ID " +
                        "JOIN Players p2 " +
                        "ON g.ID = @id AND p2.ID = g.Player2ID;";
                    command.Parameters.AddWithValue("@id", gameID);
                    command.Prepare();
                    MySqlDataReader reader = command.ExecuteReader();

                    if (reader.HasRows)
                    {
                        reader.Read();
                        record.date = (DateTime)reader[0];
                        record.player1 = (string)reader[1];
                        record.player2 = (string)reader[2];
                        record.score1 = (int)reader[3];
                        record.score2 = (int)reader[4];
                        record.time = (int)reader[5];
                        record.board = (string)reader[6];
                        reader.Close();
                    }
                    else
                    {
                        SendPageAndClose(client, "<h2>Resource not found</h2>\r\n");
                        return;
                    }

                    // get player 1's words
                    command.Parameters.Clear();
                    command.CommandText =
                        "SELECT DISTINCT Word, Legal, PlayerID FROM Words " +
                        "INNER JOIN Games " +
                        "ON Words.Game_ID = Games.ID " +
                        "WHERE Games.ID = @id AND Words.PlayerID = Games.Player1ID;";
                    command.Parameters.AddWithValue("@id", gameID);
                    command.Prepare();
                    reader = command.ExecuteReader();

                    while (reader.Read())
                    {
                        if (reader[1].ToString() == "1")
                            p1Legal.Add((string)reader[0]);
                        else
                            p1Illegal.Add((string)reader[0]);
                    }
                    reader.Close();

                    // get player 2's words
                    command.Parameters.Clear();
                    command.CommandText =
                        "SELECT DISTINCT Word, Legal, PlayerID FROM Words " +
                        "INNER JOIN Games " +
                        "ON Words.Game_ID = Games.ID " +
                        "WHERE Games.ID = @id AND Words.PlayerID = Games.Player2ID;";
                    command.Parameters.AddWithValue("@id", gameID);
                    command.Prepare();
                    reader = command.ExecuteReader();

                    while (reader.Read())
                    {
                        if (reader[1].ToString() == "1")
                            p2Legal.Add((string)reader[0]);
                        else
                            p2Illegal.Add((string)reader[0]);
                    }
                    reader.Close();

                    // pare down the lists
                    common = new List<string>(p1Legal);
                    common = new List<String>(common.Intersect(p2Legal));
                    p1Legal = new List<string>(p1Legal.Except(common));
                    p2Legal = new List<string>(p2Legal.Except(common));

                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
                finally
                {
                    connection.Close();
                }
            }
            #endregion

            #region html generation

            List<string> tableHeaders = new List<string> { "Date", "Time", "Player 1", "Player 2", "Score 1", "Score 2", "Time Limit" };

            StringBuilder sb = new StringBuilder();

            sb.Append("<html><head><title>Boggle!</title></head><body>\n");
            sb.AppendFormat("<h2>Game #{0} Summary</h2>\n", gameID);

            sb.Append("<table border=\"1\">\n<tr>\n");

            // set the headers
            foreach (string s in tableHeaders)
                sb.AppendFormat("<th width=\"80\">{0}</th>\n", s);

            sb.Append("</tr>");

            // create the rows
            sb.Append("<tr align=\"center\">");

            sb.AppendFormat("<td>{0}</td>", record.date.ToShortDateString());
            sb.AppendFormat("<td>{0}</td>", record.date.ToShortTimeString());
            sb.AppendFormat("<td>{0}</td>", record.player1);
            sb.AppendFormat("<td>{0}</td>", record.player2);
            sb.AppendFormat("<td>{0}</td>", record.score1);
            sb.AppendFormat("<td>{0}</td>", record.score2);
            sb.AppendFormat("<td>{0}</td>", record.time);

            sb.Append("</tr>");
            sb.Append("</table>");
            sb.Append("<br>");

            #region render the grid
            sb.Append("<table border=\"1\">\n");
            sb.Append("<caption align=\"center\">Board Layout</caption>\n");

            int count = 0;
            foreach (char c in record.board)
            {
                if (count % 4 == 0)
                    sb.Append("<tr height=\"25\" align=\"center\">\n");

                if (c != 'Q')
                    sb.AppendFormat("<td width=\"25\">{0}</td>", c);
                else
                    sb.AppendFormat("<td width=\"25\">{0}</td>", c + "u");

                if ((count + 1) % 4 == 0)
                    sb.Append("\n</tr>\n");

                count++;
            }

            sb.Append("</table>\n");
            #endregion

            sb.Append("<hr>");

            sb.Append("<h3>Player 1's Words</h3>\n");
            sb.AppendFormat("<b>Legal</b><br>{0}", String.Join(", ", p1Legal));
            sb.AppendFormat("<br><br><b>Illegal</b><br>{0}", String.Join(", ", p1Illegal));

            sb.Append("<hr>");

            sb.Append("<h3>Player 2's Words</h3>\n");
            sb.AppendFormat("<b>Legal</b><br>{0}", String.Join(", ", p2Legal));
            sb.AppendFormat("<br><br><b>Illegal</b><br>{0}", String.Join(", ", p2Illegal));

            sb.Append("<hr>");

            sb.Append("<h3>Common Words</h3>");
            sb.Append(String.Join(", ", common));
            sb.Append("</body></html>");
            #endregion

            SendPageAndClose(client, sb.ToString() + "\r\n");
        }

        /// <summary>
        /// Retrieves all of the players' games from the DB and returns an HTML summary.
        /// </summary>
        private void ProcessGamesRequest(StringSocket client, string name)
        {
            List<PlayerGameRecord> records = new List<PlayerGameRecord>();

            int playerID;

            #region SQL stuff
            // Connect to the DB
            using (MySqlConnection connection = new MySqlConnection(connectionString))
            {
                try
                {
                    connection.Open();
                    MySqlCommand command = connection.CreateCommand();

                    //get the ID of the target player
                    command.CommandText = "select ID from Players where Name = @name;";
                    command.Parameters.AddWithValue("@name", name);
                    command.Prepare();
                    MySqlDataReader reader = command.ExecuteReader();

                    // if the player exists, get the ID.
                    // otherwise, do something else.
                    if (reader.HasRows)
                    {
                        reader.Read();
                        playerID = (int)reader[0];
                        reader.Close();
                    }
                    else
                    {
                        reader.Close();
                        SendPageAndClose(client, "<h2>Resource not found</h2>\r\n");
                        return;
                    }

                    command.Parameters.Clear();
                    command.CommandText =
                        "SELECT Games.ID, Games.Date, Players.Name, Games.Player1Score, Games.Player2Score " +
                        "FROM Games " +
                        "JOIN Players " +
                        "ON Games.Player2ID = Players.ID AND Games.Player1ID = @id;";

                    command.Parameters.AddWithValue("@id", playerID);
                    command.Prepare();
                    reader = command.ExecuteReader();

                    while (reader.Read())
                    {
                        int id = (int)reader[0];
                        DateTime date = (DateTime)reader[1];
                        string opponent = (string)reader[2];
                        int score1 = (int)reader[3];
                        int score2 = (int)reader[4];

                        records.Add(new PlayerGameRecord(id, date, opponent, score1, score2));
                    }
                    reader.Close();

                    command.Parameters.Clear();
                    command.CommandText =
                        "SELECT Games.ID, Games.Date, Players.Name, Games.Player1Score, Games.Player2Score " +
                        "FROM Games " +
                        "JOIN Players " +
                        "ON Games.Player2ID = @id AND Games.Player1ID = Players.ID;";

                    command.Parameters.AddWithValue("@id", playerID);
                    command.Prepare();
                    reader = command.ExecuteReader();

                    while (reader.Read())
                    {
                        int id = (int)reader[0];
                        DateTime date = (DateTime)reader[1];
                        string opponent = (string)reader[2];
                        int score1 = (int)reader[3];
                        int score2 = (int)reader[4];

                        records.Add(new PlayerGameRecord(id, date, opponent, score1, score2));
                    }
                    reader.Close();

                    // order the games by ID
                    records = new List<PlayerGameRecord>(records.OrderBy(x => x.id));

                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
                finally
                {
                    connection.Close();
                }
            }
            #endregion
            
            #region html generation
            // define the table headers
            List<string> tableHeaders = new List<string> { "Game #", "Date", "Time", "Opponent", "Score", "Opponent's Score" };

            StringBuilder sb = new StringBuilder();
            
            sb.Append("<html><head><title>Boggle!</title></head><body>\n");
            sb.AppendFormat("<h2>{0}'s Games</h2>\n", name);

            sb.Append("<table border=\"1\">\n<tr>\n");

            // set the headers
            foreach (string s in tableHeaders)
            {
                sb.AppendFormat("<th width=\"100\">{0}</th>\n", s);
            }

            sb.Append("</tr>");

            // create the rows
            #region
            foreach (PlayerGameRecord r in records)
            {
                sb.Append("<tr align=\"center\">\n");

                sb.AppendFormat("<td>{0}</td>\n", r.id);
                sb.AppendFormat("<td>{0}</td>\n", r.date.ToShortDateString());
                sb.AppendFormat("<td>{0}</td>\n", r.date.ToShortTimeString());
                sb.AppendFormat("<td>{0}</td>\n", r.opponent);
                sb.AppendFormat("<td>{0}</td>\n", r.score1);
                sb.AppendFormat("<td>{0}</td>\n", r.score2);

                sb.Append("</tr>\n");
            }
            #endregion

            sb.Append("</table>\n");
            sb.Append("</body>\n");
            sb.Append("</html>");
            #endregion

            SendPageAndClose(client, sb.ToString() + "\r\n");
        }        
    }

    #region structs
    struct PlayerRecord
    {
        public int id, wins, losses, ties;
        public string name;

        public PlayerRecord(int id, string name, int wins, int losses, int ties)
        {
            this.id = id;
            this.name = name;
            this.wins = wins;
            this.losses = losses;
            this.ties = ties;
        }
    }

    struct PlayerGameRecord
    {
        public int id, score1, score2;
        public string opponent;
        public DateTime date;

        public PlayerGameRecord(int id, DateTime date, string opponent, int score1, int score2)
        {
            this.id = id;
            this.date = date;
            this.opponent = opponent;
            this.score1 = score1;
            this.score2 = score2;
        }
    }

    struct GameRecord
    {
        public string player1, player2, board;
        public int score1, score2, time;
        public DateTime date;

        public GameRecord(DateTime d, string p1, string p2, int s1, int s2, string b, int t)
        {
            date = d;
            player1 = p1;
            player2 = p2;
            score1 = s1;
            score2 = s2;
            board = b;
            time = t;
        }
    }
    #endregion
}
